package com.gadberry.utility.algorithm;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import com.gadberry.utility.algorithm.construct.FlowConstruct;
import com.gadberry.utility.expression.Argument;

/**
 * This class holds the statements that make up the steps in the algorithm. <br>
 * 
 * @author Peter Lager
 *
 */
public class Algorithm {

	private List<Statement> statements = new ArrayList<Statement>();
	private HashMap<String, Argument> vars = new HashMap<String, Argument>();

	private LinkedList<FlowConstruct> stack = new LinkedList<FlowConstruct>();
	
	private int programCounter = 0;

	private boolean trace = false;

	/**
	 * Create an algorithm from a set of statements.
	 * 
	 * @param lines
	 */
	public Algorithm(List<String> lines){
		for(String line : lines)
			statements.add(new Statement(this, line));
	}

	/**
	 * Will execute a series of expressions and will return 0 (zero) if the algorithm
	 * ended normally or non-zero if an error caused the algorithm to crash.
	 * 
	 * @return 0 if algorithm ends normally, <>1 if error terminates the algorithm.
	 */
	public int execute(){
		int error = 0;
		if(statements.size() > 0){
			reset();
			programCounter = 0;
			while(programCounter >= 0 && programCounter < statements.size()){
				// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
				if(trace){
					System.out.println("# " + programCounter + "\t" + statements.get(programCounter).getExpression());
				}

				try {
					statements.get(programCounter).execute();
				} catch (InvalidStatementException e) {
					System.out.println(e.getMessage());
					System.out.println("### ALGORITHM TERMINATED DUE TO ERROR ###");
					error = 1;
					break;
				} catch (InvalidConstructException e) {
					System.out.println(e.getMessage());
					System.out.println("### ALGORITHM TERMINATED DUE TO ERROR ###");
					error = 1;
					break;
				} catch (InvalidVariableUseException e) {
					System.out.println(e.getMessage());
					System.out.println("### ALGORITHM TERMINATED DUE TO ERROR ###");
					error = 1;
					break;
				}
			}
			if(error == 0)
				System.out.println("<>---------------------------- END OF ALGORITHM\n");
		}
		return error;
	}

	/**
	 * Get the program counter. <br>
	 * Indicates the current statement being executed.  
	 * @return line number of current statement.
	 */
	public int getProgramCounter(){
		return programCounter;
	}

	/**
	 * Sets the program counter. This is used to make jumps in the code due to the 
	 * flow control constructs
	 * @param apc
	 */
	public void setProgramCounter(int apc){
		this.programCounter = apc;
	}

	/**
	 * Advances the program to te next statement
	 */
	public void incProgramCounter(){
		programCounter++;
	}

	/**
	 * Resets all statements and removes any variables created
	 * in a previous run.
	 */
	public void reset(){
		for(Statement s : statements)
			s.reset();
		vars.clear();
	}

	/**
	 * Get the value stored in a variable. If the variable does not exist then
	 * return null.
	 * @param identifier
	 * @return
	 */
	public Argument getVariable(String identifier){
		return vars.get(identifier);
	}

	/**
	 * Sets the value associated with a variable. If the variable does not exist it is created.
	 * @param identifier variable name
	 * @param value variable content
	 */
	public void setVariable(String identifier, Argument value){
		vars.put(identifier, value);
	}

	/**
	 * Get the statement at a given line number.
	 * @param ln the line number
	 * @return the statement at line 'ln' or null if the statement is non-existent.
	 */
	public Statement getStatement(int ln){
		if(ln >=0 && ln < statements.size())
			return statements.get(ln);
		else
			return null;
	}

	/**
	 * Display all the variables and their current values.
	 */
	public void printVars(){
		Set<String> keys = vars.keySet();
		System.out.println("=========== VARIABLES ===========");
		for(String var : keys){
			System.out.print(" " + var);
			for(int i = var.length(); i < 16; System.out.print(' '), i++);
			System.out.println(vars.get(var));
		}
		System.out.println("=================================");
	}

	/**
	 * Display a listing of the algorithm.
	 * @param lineNumbers include line numbers if true
	 */
	public void printListing(boolean lineNumbers){
		System.out.println("================ ALGORITHM ==============");
		for(int i = 0; i < statements.size(); i++){
			if(lineNumbers)
				System.out.print("" + i + ":\t");
			System.out.println(statements.get(i).getExpression());
		}
		System.out.println("==========================================");

	}
	
	/**
	 * Determines whether we want the program flow displayed in the console output
	 * @param trace true = turn on trace
	 */
	public void setTrace(boolean trace){
		this.trace = trace;
	}
	
	/**
	 * Used when attempting to find a match.
	 * 
	 * @param ln
	 * @return
	 */
	private boolean isValidLineNbr(int ln){
		if(ln >=0 && ln < statements.size())
			return true;
		else
			return false;
	}

	/**
	 * Looks for a matching construct forward from the current position.
	 * 
	 * @param currLine
	 * @param construct
	 * @return
	 * @throws InvalidConstructException
	 */
	public int findMatchForward(int currLine, FlowConstruct construct) throws InvalidConstructException {
		stack.clear();
		stack.addLast(construct);
		int line = currLine + 1;
		while(!stack.isEmpty() && isValidLineNbr(line)){
			FlowConstruct startConstruct = stack.peekLast();
			FlowConstruct nextConstruct = statements.get(line).getConstruct();
			// If the statement is not a construct then move onto the next line
			if(nextConstruct == FlowConstruct.OTHER){
				line++;
				continue;
			}
			// If the constructs form a match then remove the last construct off 
			// the stack and move on
			if(startConstruct.isMatchingStartFor(nextConstruct)){
				stack.removeLast();
				line++;
				continue;
			}
			// not a match so see if it is a construct start. If it is add it 
			// to the stack and move on.
			if(nextConstruct.isSTART()){
				stack.addLast(nextConstruct);
				line++;
				continue;
			}
			// If we get here then we are in trouble because the end construct 
			// is invalid at this position.
			System.out.println("<><><>");
			break;
		}
		// Point back to actual flow construct. Need to do this before testing for success
		// in case construct is on the last line.
		line--;
		// See if we were successful
		if(!stack.isEmpty() || !isValidLineNbr(line)){
			throw new InvalidConstructException(errorMessage(currLine, line));
		}
		return line;
	}
	
	/**
	 * Create the error message when a match is not found.
	 * 
	 * @param cLine current line
	 * @param nLine next line number
	 * @return error text
	 */
	private String errorMessage(int cLine, int nLine){
		StringBuilder sb = new StringBuilder("The ");
		sb.append(statements.get(cLine).getConstruct().getToken());
		sb.append(" at line " + cLine);
		if(isValidLineNbr(nLine)){
			sb.append(" does not match with " );
			sb.append(statements.get(nLine).getConstruct().getToken());
			sb.append(" at line " + nLine);
		}
		else {
			sb.append(" has no match" );
		}
		return sb.toString();
	}
}
